/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.physics;

import cg2.mathematics.MathPlane;
import cg2.mathematics.MathVector;
import cg2.mathematics.Mathematics;

/**
 * @brief Class that collects physical constants and static methods of the physic engine.
 * 
 * If not stated else, all units are SI units.
 * 
 * @see http://en.wikipedia.org/wiki/International_System_of_Units
 */
public final class Physics {
	// -----------------------------------------------------------------------------------------------------------------
	// Physical constants
	// -----------------------------------------------------------------------------------------------------------------
	
	public static final double gravity_earth = 9.816;
	public static final double gravity_moon  = 1.622;
	
	// -----------------------------------------------------------------------------------------------------------------
	// Misc methods
	// -----------------------------------------------------------------------------------------------------------------

	/**
	 * @brief Moves a ball using the given translation vector.
	 * @throws IllegalArgumentException If the translation vector does not have dimension three.
	 */
	public static void moveObject(PhysicalObject object, MathVector translation) {
		if (translation.dim != 3)
			throw new IllegalArgumentException("Given translation vector does not have dimension three.");
		object.setPosition(Mathematics.add(object.getPosition(), translation));
	}
	
	// -----------------------------------------------------------------------------------------------------------------
	// Collision handling
	// -----------------------------------------------------------------------------------------------------------------
	
	/**
	 * @brief Collision handling between a ball and a plane.
	 * 
	 * A collision between a ball and a plane is recognized if the ball is penetrating the plane and if it is
	 * approaching the plane (that is its velocity vector points from the ball center towards the plane).  The ball will 
	 * lose 25% of its energy in plane-normal direction.
	 * 
	 * @return True if there was a collision, false otherwise.
	 */
	public static boolean handleCollision(PhysicalBall ball, MathPlane plane) {
		double distance_oriented = Mathematics.distance_oriented(plane, ball.getPosition());
		double distance          = Math.abs(distance_oriented);
		if (distance >= ball.getRadius())
			return false;
		
		// check whether we are approaching the plane
		double projection = Mathematics.mul(plane.getNormal(), ball.getVelocity());
		if (distance_oriented > 0) {                       // ball is nearer to the origin than the plane
			if (projection <= 0.)
				return false;
		} else {                                           // plane is nearer to the origin than the ball
			if (projection >= 0.)
				return false;
		}
		
		// update velocity vector
		MathVector velocity = Mathematics.mirrorVector(plane, ball.getVelocity());
		velocity.add(Mathematics.mul(plane.getNormal(), projection*0.25));
		ball.setVelocity(velocity);
		
		// make sure that ball is moved out of plane
		if (distance_oriented > 0)                         // ball is nearer to the origin than the plane
			Physics.moveObject(ball, Mathematics.mul(plane.getNormal(), -(ball.getRadius()-distance)));
		else                                               // plane is nearer to the origin than the ball
			Physics.moveObject(ball, Mathematics.mul(plane.getNormal(), ball.getRadius()-distance));
		
		return true;
	}
	
	/**
	 * @brief Collision handling between a ball and a plane.
	 * @return True if there was a collision, false otherwise.
	 */
	public static boolean handleCollision(MathPlane plane, PhysicalBall ball) {
		return handleCollision(ball, plane);
	}
	
	/**
	 * @brief Collision handling between a ball and a simple obstacle.
	 * 
	 * Collision handling is nearly identical to collision handling regarding balls and planes. The only difference is,
	 * that collision handling will only be performed if the ball is not currently within some of the 'hole regions' of
	 * the obstacle.
	 * 
	 * @return True if there was a collision, false otherwise.
	 */
	public static boolean handleCollision(PhysicalBall ball, SimpleObstacle obstacle) {
		for (SimpleObstacle.Cuboid hole : obstacle.getHoles()) {
			if (hole.contains(ball.getPosition()))
				return false;
		}
		return handleCollision(ball, obstacle.getPlane());
	}
	
	/**
	 * @brief Collision handling between a ball and a plane.
	 * @return True if there was a collision, false otherwise.
	 */
	public static boolean handleCollision(SimpleObstacle obstacle, PhysicalBall ball) {
		return handleCollision(ball, obstacle);
	}
	
	/**
	 * @brief Collision handling between two balls.
	 * 
	 * A collision between two balls is only recognized if they are penetrating each other and if at least one of them
	 * is approaching the other.
	 * 
	 * @return True if there was a collision, false otherwise.
	 */
	public static boolean handleCollision(PhysicalBall ball1, PhysicalBall ball2) {
		if (Mathematics.distance(ball1.getPosition(), ball2.getPosition()) >= ball1.getRadius()+ball2.getRadius())
			return false;

		// get velocities and direction of impact
		MathVector velocity1 = ball1.getVelocity();
		MathVector velocity2 = ball2.getVelocity();
		MathVector directionOfImpact = Mathematics.sub(ball2.getPosition(), ball1.getPosition());
		directionOfImpact.normalize();
		
		// if not at least one of the balls is approaching the other we cancel the collision handling
		if (Mathematics.mul(velocity1, directionOfImpact)/velocity1.norm() <= 0
				&& Mathematics.mul(velocity2, directionOfImpact)/velocity2.norm() >= 0)
			return false;
		
		// compute the projection of the velocities in impact direction
		double impactFraction1 = Mathematics.mul(velocity1, directionOfImpact);
		double impactFraction2 = Mathematics.mul(velocity2, directionOfImpact);
		
		// compute new velocities
		double massSum  = ball1.mass + ball2.mass;
		double massDiff = ball1.mass - ball2.mass;
		double transferredFraction1 = ( massDiff*impactFraction1 + 2*ball2.mass*impactFraction2)/massSum;
		double transferredFraction2 = (-massDiff*impactFraction2 + 2*ball1.mass*impactFraction1)/massSum;
		velocity1.sub(Mathematics.mul(directionOfImpact, impactFraction1));
		velocity1.add(Mathematics.mul(directionOfImpact, transferredFraction1));
		velocity2.sub(Mathematics.mul(directionOfImpact, impactFraction2));
		velocity2.add(Mathematics.mul(directionOfImpact, transferredFraction2));
		ball1.setVelocity(velocity1);
		ball2.setVelocity(velocity2);
		
		return true;
	}
}
